Generational garbage collection is a strategy based on the observation that most objects die young, so the heap is divided into generations that are collected with different frequencies and algorithms for optimal performance.
Generational garbage collection is a fundamental optimization technique used by modern JavaScript engines like V8, SpiderMonkey, and JavaScriptCore. It's built on the empirical observation known as the 'generational hypothesis' or 'infant mortality'—most objects are allocated, used briefly, and then become unreachable very quickly. By dividing the heap into generations and collecting them separately, engines can focus their effort on the areas where garbage is most abundant, significantly reducing pause times and improving overall application performance.
Infant Mortality: Studies of running programs show that 80-95% of objects die young. Variables inside loops, temporary objects in functions, and intermediate computation results typically exist for milliseconds before becoming garbage .
Temporal Locality: Objects that survive one collection are likely to survive many more. These long-lived objects include module exports, core data structures, and cached values .
Optimization Opportunity: By treating young and old objects differently, engines can avoid repeatedly scanning long-lived objects that are unlikely to become garbage .
Real-World Example: In a typical web application, the massive object graphs representing UI components, application state, and cached data persist throughout the session, while the objects created during a single click handler or animation frame are ephemeral .
To implement generational collection, the heap is physically or logically divided into regions. In V8, the heap consists of multiple spaces including New Space (young generation) and Old Space (old generation), along with specialized spaces for code, maps, and large objects . SpiderMonkey uses a 'nursery' for young objects, while JavaScriptCore has similar generational structures. Each generation uses different collection algorithms tuned for its expected object lifetimes.
Purpose: All new objects are allocated here. This space is small (typically 1-8 MB in V8) and designed for high allocation throughput and frequent collections .
Scavenge Algorithm: Young generation collection in V8 uses a semi-space copying collector. The space is divided into two equal halves: From-space (where objects are currently allocated) and To-space (idle). During collection, live objects are copied from From-space to To-space, then the spaces are swapped .
Allocation Pointer: New objects are allocated simply by advancing a pointer within From-space. This makes allocation extremely fast—often just a single pointer increment .
Promotion (Tenuring): Objects that survive one or more young generation collections (typically 2 cycles in V8) are 'promoted' to the old generation. Promotion also happens early if To-space is nearing capacity (e.g., >25% full) .
SpiderMonkey's Nursery: Firefox uses a similar nursery with a 'tenuring' threshold. During nursery collection, all accessible objects are either promoted to the tenured heap or, if they die, simply discarded when the nursery is cleared .
Performance Characteristics: Scavenge is fast because it only processes live objects and doesn't need to scan the entire heap. However, it trades memory capacity for speed—half the young generation space is always idle .
Purpose: Objects that have survived multiple young generation collections live here. This space is much larger (can grow to hundreds of MB or GB) and collected less frequently .
Mark-Sweep Algorithm: Old generation collection uses a mark-sweep approach. Starting from root objects (global object, stack variables, registers), the collector traverses the entire object graph and marks all reachable objects .
Mark-Phase: Objects are marked recursively. To avoid deep recursion and stack overflow, many engines use a tri-color marking algorithm and a worklist for incremental processing .
Sweep-Phase: After marking, the collector sweeps through memory, freeing unmarked objects and adding their memory to free lists for future allocations .
Mark-Compact: Over time, the heap can become fragmented. Mark-compact moves all live objects together, then frees the remaining space as one contiguous block. This is more expensive but necessary for large heaps .
Incremental Marking: To prevent long 'stop-the-world' pauses, V8 uses incremental marking. The marking work is broken into small steps interleaved with JavaScript execution, conceptually similar to React Fiber's rendering model .
A critical challenge in generational collection is cross-generational references: what happens when an old object references a young object? If the old object is the only way to reach the young object, the young object would appear unreachable if we only scan the young generation's roots. To solve this, engines maintain a 'remembered set' or 'store buffer' that tracks all old-to-young references. When a reference is created from an old object to a young one, the address is recorded. During young collection, the remembered set is treated as an additional root, ensuring correct reachability without scanning the entire old generation.
The Problem: When scanning only the young generation, the engine must know about any old objects that point to young ones, otherwise those young objects might be incorrectly collected .
Write Barrier: Every time a reference is stored into an object field, the engine executes a small piece of code (write barrier) that checks if the storing object is old and the target is young. If so, it records the location .
Store Buffer: V8 maintains a store buffer that remembers all old objects that have been modified to point to young objects. During young collection, these are treated as roots .
Performance Trade-off: The write barrier adds small overhead to every object write, but it's much cheaper than scanning the entire old generation on every young collection .
V8 (Chrome/Node.js): Uses a two-generation collector. Young generation collected by Scavenger (semi-space). Old generation collected by Mark-Sweep-Compact (Orinoco). The store buffer tracks cross-gen references. Tuning available via --max-semi-space-size and --max-old-space-size .
SpiderMonkey (Firefox): Implements a nursery for young objects and a mark-and-sweep collector for the tenured heap. Has specialized handling for strings (deduplication) and uses rooting mechanisms (JS::Rooted) for embedders to protect objects from GC .
JavaScriptCore (Safari): Also generational with young and old generations. Provides JSVirtualMachine and JSManagedValue with conditional retain behavior for automatic management. Can detect system RAM to set allocation limits .
Young Generation Size: Increasing semi-space size (--max-semi-space-size) can reduce young collection frequency but increases pause time. Useful for applications with high allocation rates .
Old Generation Limit: Setting --max-old-space-size prevents Node.js processes from consuming too much memory. Important for containerized environments .
Object Promotion Tuning: The tenuring threshold (how many collections an object survives before promotion) can be adjusted. Higher thresholds keep objects in young generation longer, reducing old generation promotion rate .
Monitoring Tools: Use --trace-gc in Node.js to see GC events, process.memoryUsage() for real-time stats, and Chrome DevTools Memory panel for heap snapshots .
Developer Impact: Write code that creates short-lived objects in functions rather than attaching them to long-lived structures when possible. This aligns with the generational hypothesis and keeps garbage collection efficient .
Generational garbage collection is a textbook example of using empirical observations to optimize system performance. By acknowledging that objects have different lifetimes and collecting them accordingly, JavaScript engines achieve low-latency, high-throughput memory management. This allows developers to focus on application logic while the engine efficiently handles the complex task of memory reclamation, with pause times often measured in milliseconds even for large, long-running applications.